3-5 拦截器扩展:自定义装饰器完成接口序列化
本节封装自定义 @Serialize() 装饰器,简化序列化拦截器的使用方式,并深入讲解 class-transformer 的 plainToInstance、excludeExtraneousValues、enableImplicitConversion 等核心配置。
目标:从冗长到简洁
之前(使用内置 ClassSerializerInterceptor):
@UseInterceptors(ClassSerializerInterceptor)
@Post('signup')
async signUp(@Body() dto: SignUpDto): Promise<PublicUserDto> {
const user = await this.authService.signUp(dto);
return new PublicUserDto(user); // 需要手动实例化
}
typescript
之后(使用自定义 @Serialize() 装饰器):
@Serialize(PublicUserDto)
@Post('signup')
async signUp(@Body() dto: SignUpDto) {
const user = await this.authService.signUp(dto);
return user; // 直接返回,拦截器自动转换
}
typescript
实现自定义 SerializeInterceptor
核心方法 plainToInstance:
import { plainToInstance } from 'class-transformer';
plainToInstance(ClassConstructor, plainObject, options)
typescript
将普通 JavaScript 对象转换为指定 class 的实例,转换行为由 class-transformer 的选项和 class 上的装饰器(@Exclude、@Expose)控制。
拦截器实现:
// common/interceptors/serialize.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class SerializeInterceptor implements NestInterceptor {
constructor(
private dto: any,
private flag?: boolean, // 可选:启用严格模式
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) =>
plainToInstance(this.dto, data, {
excludeExtraneousValues: this.flag ?? false,
enableImplicitConversion: true,
}),
),
);
}
}
typescript
关键配置项:
| 选项 | 默认值 | 说明 |
|---|---|---|
excludeExtraneousValues | false | 设为 true 时,只输出带 @Expose() 的属性 |
enableImplicitConversion | false | 设为 true 时,自动将值转换为目标类型 |
strategy | - | 'excludeAll' 或 'exposeAll',控制默认行为 |
实现自定义 @Serialize() 装饰器
// common/decorators/serialize.decorator.ts
import { UseInterceptors } from '@nestjs/common';
import { SerializeInterceptor } from '../interceptors/serialize.interceptor';
export interface ClassConstructor {
new (...args: any[]): any;
}
export function Serialize(dto: ClassConstructor, flag?: boolean) {
return UseInterceptors(new SerializeInterceptor(dto, flag));
}
typescript
装饰器本质上是一个函数,返回 UseInterceptors() 的调用结果。
两种使用模式
宽松模式(默认,flag = false):
使用 @Exclude() 标记需要排除的字段,其余字段全部输出:
export class PublicUserDto {
id: string;
username: string;
@Exclude()
password: string;
}
// Controller
@Serialize(PublicUserDto)
@Get('profile')
getProfile() {
return user;
}
// 响应: { id: "1", username: "tom" } — password 被排除
typescript
严格模式(flag = true):
只输出带 @Expose() 的字段,其余全部排除:
export class PublicUserDto {
@Expose()
id: string;
@Expose()
username: string;
password: string; // 未标记 @Expose,不会输出
}
// Controller
@Serialize(PublicUserDto, true) // 开启严格模式
@Get('profile')
getProfile() {
return user;
}
// 响应: { id: "1", username: "tom" } — 只有 @Expose 字段输出
typescript
enableImplicitConversion 隐式类型转换
开启后,class-transformer 会根据 @Type() 装饰器自动进行类型转换:
import { Expose, Type } from 'class-transformer';
export class PublicUserDto {
@Expose()
@Type(() => String)
role: string; // 即使传入 number 123,也会转为 "123"
@Expose()
@Type(() => Date)
createdAt: Date; // 传入 "2024-01" 字符串,转为 Date 对象后序列化为完整日期字符串
}
typescript
效果对比:
// enableImplicitConversion: true
plainToInstance(PublicUserDto, { role: 123, createdAt: '2024-01' }, { enableImplicitConversion: true });
// → { role: "123", createdAt: "2024-01-01T00:00:00.000Z" }
// enableImplicitConversion: false
plainToInstance(PublicUserDto, { role: 123, createdAt: '2024-01' }, { enableImplicitConversion: false });
// → { role: 123, createdAt: "2024-01" } // 不转换
typescript
完整使用示例
// dto/public-user.dto.ts
import { Exclude } from 'class-transformer';
export class PublicUserDto {
id: string;
username: string;
@Exclude()
password: string;
}
// auth.controller.ts
import { Serialize } from '../common/decorators/serialize.decorator';
@Controller('auth')
export class AuthController {
@Serialize(PublicUserDto)
@Post('signup')
async signUp(@Body() dto: SignUpDto) {
return this.authService.signUp(dto);
// 响应自动按 PublicUserDto 规则过滤
// password 被排除,id 和 username 正常输出
}
}
typescript
excludeExtraneousValues 与 strategy 的关系
| 配置 | 行为 |
|---|---|
excludeExtraneousValues: false(默认) | 所有属性默认输出,@Exclude() 标记的不输出 |
excludeExtraneousValues: true | 所有属性默认不输出,只有 @Expose() 标记的才输出 |
strategy: 'excludeAll' | 等同于全局 @Exclude(),需逐个 @Expose() |
strategy: 'exposeAll' | 等同于全局 @Expose(),需逐个 @Exclude() |
excludeExtraneousValues: true 与 strategy: 'exposeAll' 互斥,不能同时使用。
小结
| 知识点 | 要点 |
|---|---|
@Serialize(dto, flag?) | 自定义装饰器,简化序列化配置 |
plainToInstance() | 将普通对象转为 class 实例 |
excludeExtraneousValues | true 时只输出 @Expose() 字段 |
enableImplicitConversion | true 时自动按 @Type() 转换类型 |
| 宽松 vs 严格模式 | 宽松模式用 @Exclude(),严格模式用 @Expose() |
| DTO 定义简化 | 不再需要 constructor + Object.assign |
↑